/*
 *  linux/mm/memory.c
 *
 *  (C) 1991  Linus Torvalds
 */

/*
 * demand-loading started 01.12.91 - seems it is high on the list of
 * things wanted, and it should be easy to implement. - Linus
 */

/*
 * Ok, demand-loading was easy, shared pages a little bit tricker. Shared
 * pages started 02.12.91, seems to work. - Linus.
 *
 * Tested sharing by executing about 30 /bin/sh: under the old kernel it
 * would have taken more than the 6M I have free, but it worked well as
 * far as I could see.
 *
 * Also corrected some "invalidate()"s - I wasn't doing enough of them.
 */

#include <signal.h>

#include <asm/system.h>

#include <linux/sched.h>
#include <linux/head.h>
#include <linux/kernel.h>

void do_exit(long code);

static inline void oom(void)
{
	printk("out of memory\n\r");
	do_exit(SIGSEGV);
}

// 重新加载页目录基址寄存器 cr3，实现刷新存放在芯片中高速缓冲中的页表数据
#define invalidate() \
__asm__("movl %%eax,%%cr3"::"a" (0))

/* these are not to be changed without changing head.s etc */
// 图13-5 主内存区域示意图
#define LOW_MEM 0x100000   // 1M，低端内存，(包含：内核使用的内存、高速缓冲区、显存和bios_rom）。LOW MEM 是主内存区可能有的最小起始位置。当系统物理内存小于或等于 6MB 时，主内存区起始于 LOW_MEM 处，此时主内存区域没有高速缓冲的后面部分，也没有RAMDISK。
#define PAGING_MEMORY (15*1024*1024) // 分页内存（可被mm模块基于分页机制管理的内存），15M（包含：部分高速缓冲区、ramdisk（可能）、主内存区）
#define PAGING_PAGES (PAGING_MEMORY>>12) //分页内存的页数
// 给定内存地址，得出mem_map索引(分页内存的页面序号)
#define MAP_NR(addr) (((addr)-LOW_MEM)>>12)
// 标识内存页被引用的次数（写时复制机制）
#define USED 100

// 判断给定线性地址是否位于当前进程的代码段中
// 参考：图13-6 进程代码和数据在其逻辑地址空间中的分布
// ((addr)+4095)&~4095：表示当前地址所在页的下一页的首地址
#define CODE_SPACE(addr) ((((addr)+4095)&~4095) < \
current->start_code + current->end_code)

// mm内存管理模块所管理的内存最大值（内核能够使用的所有内存的末端）
static long HIGH_MEMORY = 0;

// 从 from 处拷贝1页内存到 to 处
#define copy_page(from,to) \
__asm__("cld ; rep ; movsl"::"S" (from),"D" (to),"c" (1024))

// (1M以上部分)内存映射字节图（1 字节代表1页内存)，每个页面对应的字节用于标识页面当前被引用（占用）次数。
static unsigned char mem_map [ PAGING_PAGES ] = {0,};

/*
 * Get physical address of first (actually last :-) free page, and mark it
 * used. If no free pages left, return 0.
 */
// 在主内存区中获取一个空闲物理页面。如果己经没有可用物理内存页面，则返回0。否则对页面清零，并返回空闲物理页面的物理地址
unsigned long get_free_page(void)
{
register unsigned long __res asm("ax");
/* 
逆序重复比较AL(值为0)与mem_map[]内存字节位图的每个字符，直到ECX为0或找到匹配（字节位图的字节值为0）
	如果停止条件为ECX=0：此时零标志ZF未置位（ZF=0），jne条件满足，跳转标号1处执行。
	如果停止条件为scasb匹配的值：此时scasb将零标志ZF置位(ZF=1)，jne不满足跳转条件，继续顺序执行后面的指令。
		执行scasb完后，exc和edi都自动减1，之后：
			ecx：匹配的空闲页面在mem_map中对应的字节（值为0）在mem_map数组中的索引值，此时mem_map[ecx]=0。
			edi：匹配的空闲页面在mem_map中对应的字节的前1个字节的索引值，所以[edi]+1：表示匹配的值为0的字节，等同于mem_map[ecx]
*/
__asm__("std ; repne ; scasb\n\t"
/* 如果没有等于 0 的字节，则跳转结束（返回0） */	"jne 1f\n\t"
/* 此时，[edi]+1为mem_map中值为0的字节。将[edi]+1内存赋值为1，表示引用数+1 ([edi]+1是因为执行力scasb后edi会自动-1)*/	"movb $1,1(%%edi)\n\t"
/* ecx为mem_map中字节为0的元素索引。左移12位 = 页面号*4K = 相对LOW_MEM的页面起始地址 */	"sall $12,%%ecx\n\t"
/* LOW_MEM + 相对地址 = 绝对地址，即匹配的空闲页面实际物理起始地址 */	"addl %2,%%ecx\n\t"
/* edx:匹配的空闲页面实际物理起始地址 */	"movl %%ecx,%%edx\n\t"
/* 下面3行将页面内存清0 */	"movl $1024,%%ecx\n\t"
/* 空闲页面的最后一个4字节内存单元 */	"leal 4092(%%edx),%%edi\n\t"
	"rep ; stosl\n\t"
/* eax:匹配的空闲页面实际物理起始地址 */	" movl %%edx,%%eax\n"
	"1: cld"
	:"=a" (__res)
	:"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES),
/* mem_map[]内存字节位图的最后一个字节， */	"D" (mem_map+PAGING_PAGES-1)
	);
return __res;
}

/*
 * Free a page of memory at physical address 'addr'. Used by
 * 'free_page_tables()'
 */
// 将 内存字节图 对应的页引用计数减1：mem_map[addr]--
void free_page(unsigned long addr)
{
	if (addr < LOW_MEM) return;
	if (addr >= HIGH_MEMORY)
		panic("trying to free nonexistent page");
	addr -= LOW_MEM;
	addr >>= 12; /* 得到字节图的索引 */
	if (mem_map[addr]--) return;
	mem_map[addr]=0;
	panic("trying to free free page");
}

/*
 * This function frees a continuos block of page tables, as needed
 * by 'exit()'. As does copy_page_tables(), this handles only 4Mb blocks.
 */
// 释放页表页（将某一个(或多个)页目录项置0，并释放该页目录项对应的页表页、及页表对应的页内存）
// 参数：from 起始线性基地址；size 释放的字节长度。
int free_page_tables(unsigned long from,unsigned long size)
{
	unsigned long *pg_table;
	unsigned long * dir, nr;

	if (from & 0x3fffff) /* from必须是 4M 对齐的地址，即页边界 */
		panic("free_page_tables called with wrong alignment");
	if (!from) /* 如果from为0，则表示释放页目录所在的页 */
		panic("Trying to free up swapper memory space");
	size = (size + 0x3fffff) >> 22; /* size/4M ,向上取整，表示size个页目录项 */
	dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 , 页目录中的偏移值（即页目录项的地址） */
	for ( ; size-->0 ; dir++) {
		if (!(1 & *dir)) /* 0号为P标识如果为0，表示该页目录项不存在 */
			continue;
		pg_table = (unsigned long *) (0xfffff000 & *dir); /* 取页表的叶帧地址 */
		for (nr=0 ; nr<1024 ; nr++) {
			if (1 & *pg_table)
				free_page(0xfffff000 & *pg_table); /* 释放页表项对应的页 */
			*pg_table = 0;
			pg_table++;
		}
		// 将页目录项对应的页表释放
		free_page(0xfffff000 & *dir);
		// 将页目录项置0
		*dir = 0;
	}
	// 刷新高速缓存中的页表数据
	invalidate();
	return 0;
}

/*
 *  Well, here is one of the most complicated functions in mm. It
 * copies a range of linerar addresses by copying only the pages.
 * Let's hope this is bug-free, 'cause this one I don't want to debug :-)
 *
 * Note! We don't copy just any chunks of memory - addresses have to
 * be divisible by 4Mb (one page-directory entry), as this makes the
 * function easier. It's used only by fork anyway.
 *
 * NOTE 2!! When from==0 we are copying kernel space for the first
 * fork(). Then we DONT want to copy a full page-directory entry, as
 * that would lead to some serious memory waste - we just copy the
 * first 160 pages - 640kB. Even that is more than we need, but it
 * doesn't take any more memory - we don't copy-on-write in the low
 * 1 Mb-range, so the pages can be shared with the kernel. Thus the
 * special case for nr=xxxx.
 */

// 复制页目录表项和页表项。
// 此处只是复制页目录表项和页表项，原物理内存区将被共享，未真正分配。此后两个进程（父进程和其子进程）将共享内存区，直到有一个进程执行写操作时，内核才会为写操作进程分配新的内存页（写时复制机制的实现）
int copy_page_tables(unsigned long from,unsigned long to,long size)
{
	unsigned long * from_page_table;
	unsigned long * to_page_table;
	unsigned long this_page;
	unsigned long * from_dir, * to_dir;
	unsigned long nr;

	if ((from&0x3fffff) || (to&0x3fffff))
		panic("copy_page_tables called with wrong alignment");
	// 目录项的偏移地址（也是页目录项的物理地址）
	from_dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */
	to_dir = (unsigned long *) ((to>>20) & 0xffc);
	// 页目录项个数
	size = ((unsigned) (size+0x3fffff)) >> 22;
	// 循环操作页表页
	for( ; size-->0 ; from_dir++,to_dir++) {
		if (1 & *to_dir)
			panic("copy_page_tables: already exist");
		if (!(1 & *from_dir))
			continue;
		// 页表的物理地址 = （页表中）第1个页表项的物理地址 = 目录项中的叶帧地址（即页表页的物理地址）
		from_page_table = (unsigned long *) (0xfffff000 & *from_dir);
		if (!(to_page_table = (unsigned long *) get_free_page()))
			return -1;	/* Out of memory, see freeing */
		// 设置目的目录项信息，把最后 3位置位：对应页表映射的内存页面是用户级的，并且可读写、存在(Usr，R/w， Present)
		*to_dir = ((unsigned long) to_page_table) | 7;
		// 针对当前处理的页目录项对应的页表，设置需要复制的页面项数。
		// 如果是在内核空间，则仅需复制头160 页对应的页表项 (nr = 160），对应于开始 640KB 物理内存。
		// 否则需要复制一个页表中的所有1024个页表项
		nr = (from==0)?0xA0:1024;
		// 循环操作每个页表中的页表项
		for ( ; nr-- > 0 ; from_page_table++,to_page_table++) {
			// 页表项
			this_page = *from_page_table;
			if (!(1 & this_page))
				continue;
			this_page &= ~2; // （不可写）只读
			*to_page_table = this_page;
			// 如果是非内核内存
			if (this_page > LOW_MEM) {
				*from_page_table = this_page; /* 将父进程的页表项设置为只读 */
				this_page -= LOW_MEM;
				this_page >>= 12;
				mem_map[this_page]++; /* mem_map对应页的引用计数+1 */
			}
		}
	}
	invalidate();
	return 0;
}

/*
 * This function puts a page in memory at the wanted address.
 * It returns the physical address of the page gotten, 0 if
 * out of memory (either when trying to access page-table or
 * page.)
 */
// 把一物理内存页面映射到线性地址空间指定处。或者说是把线性地址空间中指定地址 address 处的页面映射到主内存区页面 page 上。或者说将给定的page和adress建议映射关系
// 主要工作是在相关页目录项和页表项中设置指定页面的信息。若成功，返回页面的物理地址，如果内存不够(在访问页表或页面时），则返回0。
// 参数 page 是分配的主内存区中某一页面（页帧，页框）的指针；address 是指定的线性地址。
unsigned long put_page(unsigned long page,unsigned long address)
{
	unsigned long tmp, *page_table;

/* NOTE !!! This uses the fact that _pg_dir=0 */
/**注意！！!这里使用了页目录基址_pg_dir=0 的条件 */

	if (page < LOW_MEM || page >= HIGH_MEMORY)
		printk("Trying to put page %p at %p\n",page,address);
	if (mem_map[(page-LOW_MEM)>>12] != 1)
		printk("mem_map disagrees with %p at %p\n",page,address);
	// 页目录项物理地址
	page_table = (unsigned long *) ((address>>20) & 0xffc);
	// 判断该页目录项的Present标识是否为1
	if ((*page_table)&1)
		// 如果存在
		// 页表的物理地址
		page_table = (unsigned long *) (0xfffff000 & *page_table);
	else {
		// 如果页目录项缺页
		if (!(tmp=get_free_page()))
			return 0;
		// 1、更新页目录项：指向新页表物理地址
		*page_table = tmp|7;
		// 页表的物理地址
		page_table = (unsigned long *) tmp;
	}
	// 2、更新页表项：指向叶帧物理地址
	page_table[(address>>12) & 0x3ff] = page | 7;
/* no need for invalidate */
	return page;
}

// 取消写保护页面 (un_writeProtect_page)，用于页异常中断过程中写保护异常的处理（写时复制）
	// 在内核创建进程时，新进程与父进程被设置成共享代码和数据内存页面，并且所有这些页面均被设置成只读页面。
	// 而当新进程或原进程需要向内存页面写数据时，CPU 就会检测到这个情况并产生页面写保护异常。
	// 于是在这个函数中内核就会首先判断要写的页面是否被共享。
		// 若没有则把页面设置成可写然后退出。
		// 若页面是出于共享状态，则需要重新申请一新页面并复制被写页面内容，以供写进程单独使用。新、老页面的共享被取消。
// 输入参数 table_entry 为页表项指针（是物理地址）。
void un_wp_page(unsigned long * table_entry)
{
	unsigned long old_page,new_page;

	old_page = 0xfffff000 & *table_entry;
	// 如果页面仅被引用1次，页面没有被共享。
	// 即如果该内存页面此时只被一个进程使用， 并且不是内核中的进程，就直接把属性改为可写即可，不用再重新申请一个新页面。
	if (old_page >= LOW_MEM && mem_map[MAP_NR(old_page)]==1) {
		*table_entry |= 2; /* 将页表项的第2位设置为1，即 页面可写 */
		invalidate();
		return;
	}
	// 否则就需要在主内存区内申请一页空闲页面给执行写操作的进程单独使用，取消页面共享（取消只读）。
	if (!(new_page=get_free_page()))
		oom();
	if (old_page >= LOW_MEM)
		mem_map[MAP_NR(old_page)]--;
	// 更新页表项，取消只读
	*table_entry = new_page | 7;
	invalidate();
	// 拷贝页面
	copy_page(old_page,new_page);
}

/*
 * This routine handles present pages, when users try to write
 * to a shared page. It is done by copying the page to a new address
 * and decrementing the shared-page counter for the old page.
 *
 * If it's in code space we exit with a segment error.
 */
// 取消写保护页面。用于页异常中断过程中写保护异常的处理（写时复制的复制操作）
// 参数 error_code 是进程在写写保护页面时由 CPU 自动产生，address是页面线性地址。
void do_wp_page(unsigned long error_code,unsigned long address)
{
#if 0
/* we cannot do this yet: the estdio library writes to code space */
/* stupid, stupid. I really want the libc.a from GNU */
	if (CODE_SPACE(address))
		do_exit(SIGSEGV);
#endif
	// 参数处理：将线性地址address转换成页表项指针
	un_wp_page((unsigned long *)
		(((address>>10) & 0xffc) + (0xfffff000 &
		*((unsigned long *) ((address>>20) &0xffc)))));

}

// 
// 结合 图 4-17 线性地址和物理地址之间的变换
// 线性地址 = 目录项索引(偏移) + 页表项索引(偏移) + 页内偏移
// 图4-17 示出了二级表的查找过程。其中 CR3 寄存器指定页目录表的基地址。线性地址的高10 位用于索引这个页目录表，以获得指向相关第二级页表的指针。线性地址中间10 位用于索引二级页表，以获得物理地址的高20位。线性地址的低12 位直接作为物理地址低 12 位，从而组成一个完整的32 位物理地址。
void write_verify(unsigned long address)
{
	unsigned long page;

	// 判断页目录项是否存在
	// (前提：cr3寄存器设置为0x0，即页表基地址为0地址。实现对等映射，即物理地址=线性地址)
		// address>>20 为 address>>22 后在右移2位。address>>22为页目录中目录项索引，再右移2位为目录项在页目录中的偏移地址（每个目录项占用4字节）。
		// & 0xffc: 表示将低2位复位。
		// ((address>>20) & 0xffc)：等同于先将address先右移22位再乘以4，然后取低12位的值，表示目录项（在页目录中的）偏移地址。
	 // 判断指定地址所对应页目录项的页表页是否存在(P)，若不存在(P=0)则返回。
	if (!( (page = *((unsigned long *) ((address>>20) & 0xffc)) )&1)) /* 位0：Present*/
		return;
	// 取页表的地址，加上指定地址的页面在页表中的页表项偏移值，得对应物理页面的页表项指针
	// 此时，page为页目录表项
	page &= 0xfffff000; /* 此时，page为物理页帧地址（即下一级页表的基地址） */
	// ((address>>10) & 0xffc)：得到页表项偏移地址。类似address先右移12位，再乘以4，然后取低12位的值
	page += ((address>>10) & 0xffc); /* 页表项地址 = 叶帧地址 + 页内偏移地址 */
	// 此时，page为对应物理页面的页表项指针
	if ((3 & *(unsigned long *) page) == 1)  /* non-writeable, present 页面存在且不可写（只读保护）*/
		un_wp_page((unsigned long *) page);
	return;
}

// 取得一页空闲内存页，并映射到指定线性地址处。
void get_empty_page(unsigned long address)
{
	unsigned long tmp;

	if (!(tmp=get_free_page()) || !put_page(tmp,address)) {
		free_page(tmp);		/* 0 is ok - ignored */
		oom();
	}
}

/*
 * try_to_share() checks the page at address "address" in the task "p",
 * to see if it exists, and if it is clean. If so, share it with the current
 * task.
 *
 * NOTE! This assumes we have checked that p != current, and that they
 * share the same executable.
 */
// 尝试对当前进程指定地址处的页面进行共享处理，即当前进程共享进程p（一般为父进程）的指定页面。（写时复制技术的共享实现）
// 在任务p中检查位于地址”address”处的页面，看页面是否存在、是否干净。如果是干净的话，就与当前任务共享。
// 参数 address 是进程中的逻辑地址（准确说是逻辑地址中的偏移地址），即是当前进程欲与p进程共享页面的逻辑页面地址。
// 参数 p 是将被共享页面的进程（p是当前进程的父进程parent）。
// 返回值为成功共享的页面数量：0 - 共享失败，共享了0个页面；1 - 共享成功了1个页面；
static int try_to_share(unsigned long address, struct task_struct * p)
{
	unsigned long from;
	unsigned long to;
	unsigned long from_page;
	unsigned long to_page;
	unsigned long phys_addr;

	/* 
	背景：当前进程与进程p是同一执行代码（父子进程），也可以认为当前进程是由p进程执行 fork 操作产生的进程，因此它们的代码内容一样。
	如果未对数据段内容作过修改那么数据段内容也应一样。

	逻辑地址的本质：CPU 4G 线性空间中的 线性地址 = 段基址 + 段内部线性空间中的“线性地址”址（即偏移地址）
	首先分别求得指定进程p中和当前进程中逻辑地址 address 对应的页目录项：
	1. 为了计算方便先求出指定逻辑地址 address 处的“逻辑”页目录项号，即以进程空间（0-64MB)算出的页目录项号。
	2. 该“逻辑”页目录项号加上进程p在CPU 4G 线性空间中起始地址对应的页目录项，即得到进程p中地址 address 处页面所对应的 4G 线性空间中的实际页目录项 from_page。
	同理，“逻辑”页目录项号加上当前进程在CPU 4G 线性空间中起始地址对应的页目录项，即可最后得到当前进程中地址 address 处页面所对应的 4G 线性空间中的实际页目录项 to_page。
	*/
	// 内存段内部线性空间中的页目录项
	from_page = to_page = ((address>>20) & 0xffc);
	// 4G 线性空间中的页目录项（指针）
	from_page += ((p->start_code>>20) & 0xffc);
	to_page += ((current->start_code>>20) & 0xffc);
/* is there a page-directory at from? */
	// 页目录项（p进程）
	from = *(unsigned long *) from_page;
	// 如果页目录项缺页
	if (!(from & 1))
		return 0;
	// 页表叶帧地址（p进程）
	from &= 0xfffff000;
	// 页表项偏移地址（p进程）
	from_page = from + ((address>>10) & 0xffc);
	// 页表项内容（p进程）
	phys_addr = *(unsigned long *) from_page;
/* is the page clean and present? */ 
	// 如果页面不干净（脏页）或无效（不存在）则返回。0x41 对应页表项中的D(Dirty)和P (Present) 标志
	if ((phys_addr & 0x41) != 0x01)
		return 0; // 脏页不能共享；不存在的页也不能共享
	// 内存页的叶帧地址
	phys_addr &= 0xfffff000;
	if (phys_addr >= HIGH_MEMORY || phys_addr < LOW_MEM)
		return 0;
	// 页目录项内容（当前进程）
	to = *(unsigned long *) to_page;
	if (!(to & 1)) {
		if ((to = get_free_page()))
			// 分配了内存页后，要更新页表项
			*(unsigned long *) to_page = to | 7;
		else
			oom();
	}
	// 页表页帧地址（当前进程）
	to &= 0xfffff000;
	// 页表项偏移地址（当前进程）
	to_page = to + ((address>>10) & 0xffc);
	// 如果当前进程的目标页已经存在
	if (1 & *(unsigned long *) to_page)
		panic("try_to_share: to_page already exists");
/* share them: write-protect */
	// 如果当前进程的address指定的页缺页
	*(unsigned long *) from_page &= ~2; /* 设置为只读页（写保护） */
	*(unsigned long *) to_page = *(unsigned long *) from_page; /* 共享进程p的页面 */
	invalidate();
	phys_addr -= LOW_MEM;
	phys_addr >>= 12;
	mem_map[phys_addr]++; /* 更新引用计数 */
	return 1;
}

/*
 * share_page() tries to find a process that could share a page with
 * the current one. Address is the address of the wanted page relative
 * to the current data space.
 *
 * We first check if it is at all feasible by checking executable->i_count.
 * It should be >1 if there are other tasks sharing this inode.
 */
// 试图找到一个进程（该进程与当前进程运行同一个执行文件），给当前进程共享页面执行文件。（注意共享的是执行文件，不是堆栈数据）
// 参数 address 是当前进程数据空间中期望共享的某页面地址。是进程中的逻辑地址，即是当前进程欲与p进程共享页面的逻辑页面地址。
// 返回值为成功共享的页面数量：0 - 共享失败，共享了0个页面；1 - 共享成功了1个页面；
static int share_page(unsigned long address)
{
	struct task_struct ** p;
	/**
	使用场景：
	在发生缺页异常时，首先看看能否与运行同一个执行文件的其他进程进行页面共享处理。
	该函数首先判断系统中是否有另一个进程也在运行当前进程一样的执行文件。若有，则在系统当前所有任务中寻找这样的任务。若找到了这样的任务就尝试与其共享指定地址处的页面。
	若系统中没有其他任务正在运行与当前进程相同的执行文件，那么共享页面操作的前提条件不存在，因此函数立刻退出。
	 */
	if (!current->executable)
		return 0;
	// 判断系统中是否有另一个进程也在运行当前进程一样的执行文件
	if (current->executable->i_count < 2)
		return 0;
	for (p = &LAST_TASK ; p > &FIRST_TASK ; --p) {
		if (!*p)
			continue;
		if (current == *p)
			continue;
		if ((*p)->executable != current->executable)
			continue;
		if (try_to_share(address,*p))
			return 1;
	}
	return 0;
}

// 执行缺页处理（缺页中断时调用）
// 函数参数 error_code 和 address是进程在访问页面时由 CPU 因缺页产生异常而自动生成。
// error_code 指出出错类型；address 是产生异常的页面线性地址。
void do_no_page(unsigned long error_code,unsigned long address)
{
	int nr[4];
	unsigned long tmp;
	unsigned long page;
	int block,i;

	// 清除线性地址中的偏移地址部分，进行4K对齐，获取叶帧地址
	address &= 0xfffff000;
	// 缺页页面对应逻辑地址。（缺页的段内偏移地址）
	tmp = address - current->start_code;
	// 由于任务0和任务1的代码在内核中，因此任务0、任务1以及任务1派生的没有调用过 execve() 的所有任务的 executable 都为0。
	// 若executable值为0，或者参数指定的线性地址超出代码加数据长度，则表明进程在申请新的内存页面存放堆或栈中数据。
	// 因此直接调用取空闲页面函数 get_empty_page(）为进程申请一页物理内存并映射到指定线性地址
	if (!current->executable || tmp >= current->end_data) {
		get_empty_page(address);
		return;
	}
	// 否则说明所缺页面在进程执行影像文件范围内，于是就尝试共享页面操作，若成功则退出。
	// 若不成功就只能申请一页物理内存页面 page，然后从设备上读取执行文件中的相应页面并放置（映射）到进程页面逻辑地址 tmp 处。
	if (share_page(tmp))
		return;

	// 获取一个空闲页面，然后从设备上读取执行文件中的相应页面
	if (!(page = get_free_page()))
		oom();
/* remember that 1 block is used for header */
/* 记住，（程序）头要使用1个数据块 */
	block = 1 + tmp/BLOCK_SIZE;
	for (i=0 ; i<4 ; block++,i++)
		nr[i] = bmap(current->executable,block);
	bread_page(page,current->executable->i_dev,nr);
	i = tmp + 4096 - current->end_data;
	tmp = page + 4096;
	while (i-- > 0) {
		tmp--;
		*(char *)tmp = 0;
	}
	if (put_page(page,address))
		return;
	free_page(page);
	oom();
}

// 设置HIGH_MEMORY；并初始化mem_map数组（内存页字节图）
void mem_init(long start_mem, long end_mem)
{
	// 使用一个mem_map数组进行初始化（1M-15M），每一个元素表示一个页的使用情况。默认值为0，此处将其重置为100，表示已经使用。
	// 初始化后的结果：主内存区域对应的MAP元素为0（未使用），高速缓冲区和虚拟盘（如果存在）对应的MAP元素为100（已使用），设置HIGH_MEMORY
	int i;
	HIGH_MEMORY = end_mem;
	for (i=0 ; i<PAGING_PAGES ; i++)
		mem_map[i] = USED;
	i = MAP_NR(start_mem); 
	end_mem -= start_mem; // 主内存区长度
	end_mem >>= 12;  // 主内存区页数
	while (end_mem-- > 0)
		mem_map[i++]=0;  // 将主内存的MAP元素重置为0，表示未分配使用
}

// 计算内存空闲页面数并显示。
// [?？内核中没有地方调用该函数，Linus 调试过程中用的]
void calc_mem(void)
{
	int i,j,k,free=0;
	long * pg_tbl;

	for(i=0 ; i<PAGING_PAGES ; i++)
		if (!mem_map[i]) free++;
	printk("%d pages free (of %d)\n\r",free,PAGING_PAGES);
	for(i=2 ; i<1024 ; i++) {
		if (1&pg_dir[i]) {
			pg_tbl=(long *) (0xfffff000 & pg_dir[i]);
			for(j=k=0 ; j<1024 ; j++)
				if (pg_tbl[j]&1)
					k++;
			printk("Pg-dir[%d] uses %d pages\n",i,k);
		}
	}
}
